﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace CoffeeChat
{

    public sealed class Command
    {
        public static readonly UTF8Encoding CurrentEncoding = new UTF8Encoding();
        private static readonly byte[] BinaryZero = { 0, 0, 0, 0 };

        private byte _intent;
        public byte[] UID { get; private set; }
        private List<KeyValuePair<string, string>> _baseData;
        private byte[] _extraData;

        public byte Intent
        {
            get
            {
                return this._intent;
            }
            set
            {
                this._intent = value;
            }
        }
        public byte[] ExtraData
        {
            get
            {
                return this._extraData;
            }
            set
            {
                if ( value == null )
                {
                    this._extraData = new byte[0];
                }
                else
                {
                    this._extraData = value;
                }
            }
        }

        public Command()
        {
            this._extraData = new byte[0];
            this._baseData = new List<KeyValuePair<string, string>>();
            this.UID = Guid.NewGuid().ToByteArray();
        }
        public Command(int prediectedCapacity)
        {
            this._extraData = new byte[0];
            this._baseData = new List<KeyValuePair<string, string>>( prediectedCapacity );
            this.UID = Guid.NewGuid().ToByteArray();
        }

        public int Count
        {
            get
            {
                return this._baseData.Count;
            }
        }

        public void Add(string key, string value)
        {
            if ( key != null )
            {
                if ( value == null )
                    value = string.Empty;
                this._baseData.Add( new KeyValuePair<string, string>( key, value ) );
            }
        }

        public void SetItem(string key, string value)
        {
            KeyValuePair<string, string> newItem = new KeyValuePair<string, string>( key, value );
            for ( int i = 0; i < this._baseData.Count; i++ )
            {
                if ( string.Equals( this._baseData[i].Key, key, StringComparison.Ordinal ) )
                {
                    this._baseData[i] = newItem;
                    return;
                }
            }
            this._baseData.Add( newItem );
        }
        public string GetItem(string key)
        {
            for ( int i = 0; i < this._baseData.Count; i++ )
            {
                if ( string.Equals( this._baseData[i].Key, key, StringComparison.Ordinal ) )
                {
                    return this._baseData[i].Value;
                }
            }
            return null;
        }

        public string[] GetManyItems(string key)
        {
            string[] returned = new string[this._baseData.Count];
            int counted = 0;
            if ( key != null )
            {
                for ( int i = 0; i < returned.Length; i++ )
                {
                    if ( string.Equals( this._baseData[i].Key, key, StringComparison.Ordinal ) )
                    {
                        returned[counted] = this._baseData[i].Value;
                        counted++;
                    }
                }
            }
            if ( returned.Length != counted )
            {
                string[] resized = new string[counted];

                Array.Copy( returned, 0, resized, 0, counted );
                return resized;
            }
            return returned;
        }

        public byte[] Serialize()
        {
            MemoryStream data = new MemoryStream();

            data.WriteByte( this._intent );

            byte[] tempBuffer;

            try
            {

                for ( int i = 0; i < this._baseData.Count; i++ )
                {

                    tempBuffer = Command.CurrentEncoding.GetBytes( this._baseData[i].Key );

                    data.Write( BitConverter.GetBytes( tempBuffer.Length ), 0, 4 );

                    data.Write( tempBuffer, 0, tempBuffer.Length );


                    tempBuffer = Command.CurrentEncoding.GetBytes( this._baseData[i].Value );

                    data.Write( BitConverter.GetBytes( tempBuffer.Length ), 0, 4 );

                    data.Write( tempBuffer, 0, tempBuffer.Length );

                }

                data.Write( Command.BinaryZero, 0, 4 );

                data.Write( this._extraData, 0, this._extraData.Length );

                return data.ToArray();

            }
            finally
            {
                data.Dispose();
            }
        }

        public static Command Deserialize(byte[] rawData)
        {
            Command returned = new Command();
            if ( rawData != null && rawData.Length > 0 )
            {
                returned._intent = rawData[0];

                int i = 1;

                int tempCount;
                byte[] tempBuffer4 = new byte[4];
                byte[] tempBufferDataA;
                byte[] tempBufferDataB;
                while ( i < rawData.Length )
                {
                    tempBuffer4[0] = rawData[i];
                    i++;
                    tempBuffer4[1] = rawData[i];
                    i++;
                    tempBuffer4[2] = rawData[i];
                    i++;
                    tempBuffer4[3] = rawData[i];
                    i++;
                    tempCount = BitConverter.ToInt32( tempBuffer4, 0 );


                    if ( tempCount == 0 )
                    {
                        returned._extraData = new byte[rawData.Length - i];
                        for ( int ed = 0; ed < returned._extraData.Length; ed++ )
                        {
                            returned._extraData[ed] = rawData[ed + i];
                        }
                        i += returned._extraData.Length;
                    }
                    else
                    {
                        if ( tempCount < 1 )
                        {
                            break;
                        }

                        tempBufferDataA = new byte[tempCount];
                        for ( int b = 0; b < tempBufferDataA.Length; b++ )
                        {
                            tempBufferDataA[b] = rawData[b + i];
                        }
                        i += tempBufferDataA.Length;

                        tempBuffer4[0] = rawData[i];
                        i++;
                        tempBuffer4[1] = rawData[i];
                        i++;
                        tempBuffer4[2] = rawData[i];
                        i++;
                        tempBuffer4[3] = rawData[i];
                        i++;
                        tempCount = BitConverter.ToInt32( tempBuffer4, 0 );

                        if ( tempCount < 1 )
                        {
                            break;
                        }

                        tempBufferDataB = new byte[tempCount];
                        for ( int b = 0; b < tempBufferDataB.Length; b++ )
                        {
                            tempBufferDataB[b] = rawData[b + i];
                        }
                        i += tempBufferDataB.Length;

                        returned.Add( Command.CurrentEncoding.GetString( tempBufferDataA ), Command.CurrentEncoding.GetString( tempBufferDataB ) );

                    }
                }
            }
            return returned;
        }
    }

    public delegate void ServerDataReceivedHandler(EndPoint address, Command cmd);
    public delegate void ServerErrorFoundHandler(Exception ex);
    public delegate void ServerClientConnectedHandler(EndPoint address);
    public delegate void ServerClientDisconnectedHandler(EndPoint address);
    public delegate void ServerConnectionClosedHandler();
    public class EasyServer : IDisposable
    {
        public event ServerDataReceivedHandler DataReceived;
        public event ServerErrorFoundHandler ErrorFound;
        public event ServerClientConnectedHandler ClientConnected;
        public event ServerClientDisconnectedHandler ClientDisconnected;
        public event ServerConnectionClosedHandler ConnectionClosed;

        private object syncList;
        private SynchronizationContext _context;
        private bool _alive;
        private TcpListener _tcp;
        private List<Socket> _clients;
        private List<ServerWorkItemSend> _sendQueue;
        private bool _flagSender;
        private bool _flagSessions;
        private bool _flagPinger;

        public EasyServer(int port)
        {
            this.syncList = new object();
            this._clients = new List<Socket>( 16 );
            this._sendQueue = new List<ServerWorkItemSend>( 64 );
            this._context = SynchronizationContext.Current;

            this._tcp = new TcpListener( IPAddress.Any, port );
        }

        public void DisconnectClient(EndPoint clientEndPoint)
        {
            Socket temp;
            lock ( this.syncList )
            {
                for ( int i = this._clients.Count - 1; i >= 0; i-- )
                {
                    temp = this._clients[i];
                    if ( temp.RemoteEndPoint.Equals( clientEndPoint ) )
                    {
                        temp.Shutdown( SocketShutdown.Both );
                        temp.Close( 1000 );
                    }
                }
            }
        }
        public Socket GetClientByEndPoint(EndPoint clientEndPoint)
        {
            Socket tempClient;
            lock ( this.syncList )
            {
                for ( int i = this._clients.Count - 1; i >= 0; i-- )
                {
                    tempClient = this._clients[i];
                    if ( object.Equals( tempClient.RemoteEndPoint, clientEndPoint ) )
                    {
                        return tempClient;
                    }
                }
            }
            return null;
        }

        public void StartConnection()
        {
            try
            {
                this._tcp.Start( 65536 );
                this._alive = true;
            }
            catch ( SocketException ex )
            {
                this._alive = false;
                handleErrorFound( ex );
                handleConnectionClosed();
            }
            if ( this._alive )
            {
                Thread sessionReader;
                sessionReader = new Thread( new ThreadStart( this.backgroundSessions ) );
                sessionReader.Start();
                Thread sendThread;
                sendThread = new Thread( new ThreadStart( this.backgroundSender ) );
                sendThread.Start();
                Thread pingSender;
                pingSender = new Thread( new ThreadStart( this.backgroundPinger ) );
                //pingSender.Start();
            }
        }
        public void CloseConnection()
        {
            this._alive = false;
            Thread tryToClose = new Thread( new ThreadStart( this.InternalCloseConnection ) );
            tryToClose.Start();
        }
        private void InternalCloseConnection()
        {
            while ( this._flagSender | this._flagSessions | this._flagPinger )
            {
                Thread.Sleep( 1000 );
            }
            try
            {
                this._tcp.Stop();
            }
            catch ( SocketException ex )
            {
                handleErrorFound( ex );
            }
            finally
            {
                handleConnectionClosed();
            }
        }

        public void SendData(EndPoint client, Command cmd)
        {
            if ( client != null )
            {
                if ( cmd == null )
                    throw new ArgumentNullException( "cmd" );

                if ( this._alive )
                {
                    ServerWorkItemSend newItem = new ServerWorkItemSend();
                    newItem.CommandToSend = cmd;
                    newItem.Who = client;
                    this._sendQueue.Add( newItem );
                }
            }
        }
        public void SendDataToAll(Command cmd)
        {
            if ( cmd == null )
                throw new ArgumentNullException( "cmd" );

            if ( this._alive )
            {
                ServerWorkItemSend newItem = new ServerWorkItemSend();
                newItem.CommandToSend = cmd;
                Socket clientCache;
                for ( int i = 0; i < this._clients.Count; i++ )
                {
                    clientCache = this._clients[i];
                    if ( clientCache.Connected )
                    {
                        newItem.Who = clientCache.RemoteEndPoint;
                        this._sendQueue.Add( newItem );
                    }
                }
            }
        }

        private void handleDataReceived(EndPoint address, Command cmd)
        {
            if ( this.DataReceived != null )
            {
                if ( this._context != null )
                {
                    this._context.Post( (object s) => { this.DataReceived( address, cmd ); }, null );
                }
                else
                {
                    this.DataReceived( address, cmd );
                }
            }
        }
        private void handleErrorFound(Exception ex)
        {
            if ( this.ErrorFound != null )
            {
                if ( this._context != null )
                {
                    this._context.Post( (object s) => { this.ErrorFound( ex ); }, null );
                }
                else
                {
                    this.ErrorFound( ex );
                }
            }
        }
        private void handleClientConnectionChange(EndPoint address, bool isConnected)
        {
            if ( isConnected )
            {
                if ( this.ClientConnected != null )
                {
                    if ( this._context != null )
                    {
                        this._context.Post( (object s) => { this.ClientConnected( address ); }, null );
                    }
                    else
                    {
                        this.ClientConnected( address );
                    }
                }
            }
            else
            {
                if ( this.ClientDisconnected != null )
                {
                    if ( this._context != null )
                    {
                        this._context.Post( (object s) => { this.ClientDisconnected( address ); }, null );
                    }
                    else
                    {
                        this.ClientDisconnected( address );
                    }
                }
            }
        }
        private void handleConnectionClosed()
        {
            if ( this.ConnectionClosed != null )
            {
                if ( this._context != null )
                {
                    this._context.Post( (object s) => { this.ConnectionClosed(); }, null );
                }
                else
                {
                    this.ConnectionClosed();
                }
            }
        }

        private void backgroundReader(Socket sock)
        {
            byte[] buffer;
            Command cmd;

            bool isReadingPacket = false;
            int lengthNeeded = 0;
            int amountRead = 0;

            byte[] packetData = new byte[0];

            while ( this._alive )
            {
                if ( isReadingPacket )
                {
                    buffer = new byte[1];
                    sock.Receive( buffer );
                    amountRead++;

                    packetData[amountRead - 1] = buffer[0];

                    if ( amountRead >= lengthNeeded )
                    {
                        isReadingPacket = false;
                        lengthNeeded = 0;
                        cmd = Command.Deserialize( packetData );
                        amountRead = 0;
                        packetData = null;
                        handleDataReceived( sock.RemoteEndPoint, cmd );
                    }
                }
                else
                {
                    buffer = new byte[4];
                    sock.Receive( buffer );
                    lengthNeeded = BitConverter.ToInt32( buffer, 0 );

                    if ( lengthNeeded > 16777216 )
                    {
                        break; //This class will refuse to accept packets over 16 MB.
                    }
                    else
                    {
                        if ( lengthNeeded > 0 )
                        {
                            packetData = new byte[lengthNeeded];
                            amountRead = 0;
                            isReadingPacket = true;
                        }
                    }
                }
            }
        }

        private void backgroundSender()
        {
            this._flagSender = true;
            ServerWorkItemSend temp;
            byte[] cmdData;
            Socket cmdWho;
            while ( this._alive )
            {
                try
                {
                    if ( this._sendQueue.Count > 0 )
                    {
                        for ( int i = this._sendQueue.Count - 1; i >= 0; i-- )
                        {
                            temp = this._sendQueue[i];
                            cmdData = temp.CommandToSend.Serialize();
                            cmdWho = this.GetClientByEndPoint( temp.Who );
                            this._sendQueue.Remove( temp );
                            if ( cmdWho != null )
                            {
                                byte[] cmdLength = BitConverter.GetBytes( cmdData.Length );
                                byte[] buffer = new byte[cmdData.Length + 4];
                                cmdLength.CopyTo( buffer, 0 );
                                cmdData.CopyTo( buffer, 4 );
                                cmdWho.Send( buffer );
                            }
                        }
                    }
                }
                catch ( SocketException ex )
                {
                    handleErrorFound( ex );
                }
            }
            this._flagSender = false;
        }
        private void backgroundSessions()
        {
            this._flagSessions = true;
            Socket sock;
            EndPoint address;
            while ( this._alive )
            {
                if ( this._tcp.Pending() )
                {
                    sock = this._tcp.AcceptSocket();

                    Thread bgReader = new Thread( new ThreadStart( () => this.backgroundReader( sock ) ) );
                    bgReader.Start();
                    this._clients.Add( sock );
                    handleClientConnectionChange( sock.RemoteEndPoint, true );

                }
                //Check if any clients are disconnected.
                lock ( this.syncList )
                {
                    for ( int i = this._clients.Count - 1; i >= 0; i-- )
                    {
                        if ( !this._clients[i].Connected )
                        {
                            try
                            {
                                address = this._clients[i].RemoteEndPoint;
                            }
                            catch ( ObjectDisposedException )
                            {
                                address = null;
                            }
                            this._clients.RemoveAt( i );
                            handleClientConnectionChange( address, false );
                        }
                    }
                }
            }
            this._flagSessions = false;
        }
        private void backgroundPinger()
        {
            this._flagPinger = true;
            byte[] pingPacketData = { 1, 0, 0, 0, 1 };
            //TODO: If the command parsing is modified, check this.
            while ( this._alive )
            {
                try
                {
                    lock ( this.syncList )
                    {
                        for ( int i = this._clients.Count - 1; i >= 0; i-- )
                        {
                            this._clients[i].Send( pingPacketData );
                        }
                    }
                }
                catch ( SocketException ex )
                {
                    handleErrorFound( ex );
                }
                Thread.Sleep( 3000 );
            }
            this._flagPinger = false;
        }

        public bool IsDisposed
        {
            get
            {
                return this._isDisposed;
            }
        }
        private bool _isDisposed;
        protected virtual void Dispose(bool disposing)
        {
            if ( !this._isDisposed )
            {
                if ( disposing )
                {
                    try
                    {
                        Socket sock;
                        for ( int i = 0; i < this._clients.Count; i++ )
                        {
                            sock = this._clients[i];
                            if ( sock.Connected )
                            {
                                sock.Shutdown( SocketShutdown.Both );
                                sock.Close( 1000 );
                            }
                        }
                    }
                    catch ( SocketException ) { }
                    finally
                    {
                        this._clients.Clear();
                        this._tcp.Stop();
                    }
                }
                this._isDisposed = true;
            }
        }
        public void Dispose()
        {
            this.Dispose( true );
            GC.SuppressFinalize( this );
        }

    }

    public struct ServerWorkItemSend
    {
        private Command _commandToSend;
        private EndPoint _who;
        public Command CommandToSend
        {
            get
            {
                return this._commandToSend;
            }
            set
            {
                this._commandToSend = value;
            }
        }
        public EndPoint Who
        {
            get
            {
                return this._who;
            }
            set
            {
                this._who = value;
            }
        }
    }

    public delegate void ClientDataReceivedHandler(Command cmd);
    public delegate void ClientConnectedHandler();
    public delegate void ClientDisconnectedHandler();
    public delegate void ClientErrorFoundHandler(Exception ex);
    public class EasyClient : IDisposable
    {
        private SynchronizationContext _context;
        private Socket _sock;
        public event ClientDataReceivedHandler DataReceived;
        public event ClientConnectedHandler Connected;
        public event ClientDisconnectedHandler Disconnected;
        public event ClientErrorFoundHandler ErrorFound;
        private List<Command> _sendQueue;
        private bool _alive;
        private bool _flagReader;
        private bool _flagSender;

        public bool IsConnected
        {
            get
            {
                return this._sock.Connected;
            }
        }
        public EasyClient()
        {
            this._context = SynchronizationContext.Current;
            this._sock = new Socket( AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );
            this._sendQueue = new List<Command>( 32 );
        }

        public void Connect(IPEndPoint hostIP)
        {
            if ( !this._sock.Connected )
            {
                Thread connectThread = new Thread( new ThreadStart( () =>
                {
                    try
                    {
                        this._sock.Connect( hostIP );
                    }
                    catch ( SocketException ) { }
                    finally
                    {
                        if ( this._sock.Connected )
                        {
                            this._alive = true;
                            handleConnectionChange( true );
                            Thread sendThread = new Thread( new ThreadStart( backgroundSender ) );
                            sendThread.Start();
                            this.backgroundReader();
                        }
                        else
                        {
                            handleConnectionChange( false );
                        }
                    }
                } ) );
                connectThread.Start();
            }
        }

        public void Disconnect()
        {
            if ( this._sock.Connected )
            {
                this._alive = false;
                Thread tryToDisconnect = new Thread( new ThreadStart( InternalDisconnect ) );
                tryToDisconnect.Start();
            }
        }
        private void InternalDisconnect()
        {
            while ( this._flagReader | this._flagSender )
            {
                if ( !this._sock.Connected )
                    return;
                Thread.Sleep( 1000 );
            }
            if ( this._sock.Connected )
            {
                this._sock.Shutdown( SocketShutdown.Both );
                this._sock.Close( 1000 );
                handleConnectionChange( false );
            }
        }

        public void SendData(Command cmd)
        {
            if ( cmd != null )
            {
                if ( this._sock.Connected )
                {
                    this._sendQueue.Add( cmd );
                }
            }
        }

        private void handleDataReceived(Command cmd)
        {
            if ( DataReceived != null )
            {
                if ( this._context != null )
                {
                    this._context.Post( new SendOrPostCallback( (object t) => { DataReceived( cmd ); } ), null );
                }
                else
                {
                    DataReceived( cmd );
                }
            }
        }
        private void handleErrorFound(Exception ex)
        {
            if ( ErrorFound != null )
            {
                if ( this._context != null )
                {
                    this._context.Post( new SendOrPostCallback( (object t) => { ErrorFound( ex ); } ), null );
                }
                else
                {
                    ErrorFound( ex );
                }
            }
        }
        private void handleConnectionChange(bool isConnected)
        {
            if ( isConnected )
            {
                if ( Connected != null )
                {
                    if ( this._context != null )
                    {
                        this._context.Post( new SendOrPostCallback( (object t) => { Connected(); } ), null );
                    }
                    else
                    {
                        Connected();
                    }
                }
            }
            else
            {
                if ( Disconnected != null )
                {
                    if ( this._context != null )
                    {
                        this._context.Post( new SendOrPostCallback( (object t) => { Disconnected(); } ), null );
                    }
                    else
                    {
                        Disconnected();
                    }
                }
            }
        }

        private void backgroundReader()
        {
            this._flagReader = true;
            byte[] buffer;
            Command cmd;

            bool isReadingPacket = false;
            int lengthNeeded = 0;
            int amountRead = 0;

            byte[] packetData = new byte[0];

            try
            {
                while ( this._alive )
                {
                    if ( this._sock.Available > 0 )
                    {
                        if ( isReadingPacket )
                        {
                            buffer = new byte[1];
                            this._sock.Receive( buffer );

                            packetData[amountRead] = buffer[0];
                            amountRead++;

                            if ( amountRead >= lengthNeeded )
                            {
                                isReadingPacket = false;
                                lengthNeeded = 0;
                                cmd = Command.Deserialize( packetData );
                                amountRead = 0;
                                packetData = null;
                                handleDataReceived( cmd );
                            }
                        }
                        else
                        {
                            buffer = new byte[4];
                            this._sock.Receive( buffer );

                            lengthNeeded = BitConverter.ToInt32( buffer, 0 );
                            if ( lengthNeeded > 16777216 )
                                break; //This class will refuse to accept packets over 16 MB.
                            else
                            {
                                if ( lengthNeeded > 0 )
                                {
                                    packetData = new byte[lengthNeeded];
                                    amountRead = 0;
                                    isReadingPacket = true;
                                }
                            }
                        }
                    }
                }
            }
            catch ( SocketException ex )
            {
                handleErrorFound( ex );
            }
            finally
            {
                this._flagReader = false;
            }
        }
        private void backgroundSender()
        {
            this._flagSender = true;
            byte[] cmdData;
            byte[] cmdLength;
            byte[] buffer;
            try
            {
                while ( this._sock.Connected )
                {
                    if ( this._sendQueue.Count > 0 )
                    {
                        for ( int i = this._sendQueue.Count - 1; i >= 0; i-- )
                        {
                            if ( this._sendQueue[i] != null )
                            {
                                cmdData = this._sendQueue[i].Serialize();
                                this._sendQueue.RemoveAt( i );
                                cmdLength = BitConverter.GetBytes( cmdData.Length );
                                buffer = new byte[cmdData.Length + 4];
                                cmdLength.CopyTo( buffer, 0 );
                                cmdData.CopyTo( buffer, 4 );
                                this._sock.Send( buffer );
                            }
                        }
                    }
                }
            }
            finally
            {
                buffer = null;
                this._flagSender = false;
            }
        }

        public bool IsDisposed
        {
            get
            {
                return this._isDisposed;
            }
        }
        private bool _isDisposed;
        protected virtual void Dispose(bool disposing)
        {
            if ( !this._isDisposed )
            {
                if ( disposing )
                {
                    this._sock.Close();
                }
                this._isDisposed = true;
            }
        }
        public void Dispose()
        {
            this.Dispose( true );
            GC.SuppressFinalize( this );
        }

    }


}


